#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

# Object Oriented Programming!
# ====CLASS AND INSTANCE==== 

class Student(object): # Class_name(inherited class)
    def __init__(self, name, score): # the first parameter in class method must be 'self'
        self.name = name # self point to Instance itself
        self.score = score

    def print_score(self): # define methods to manipulate data inside the class, is called encapsulation
        print('%s: %s' % (self.name, self.score))

bart = Student('Bart Simpson', 76) # define a Instance for class
# parameters except 'self' need to be claimed when a Instance is defined

bart.print_score()

# ====RESTRICTION==== 

class Student2(object):

    def __init__(self, name, score):
        self.__name = name # __xx is name of private var that cannot be access outside
        self.__score = score

    def print_score(self): # use method to achieve controlable access to private var
        print('%s: %s' % (self.__name, self.__score))
    
    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')
    

# ====INHERIT AND POLYMORPH==== 

class Animal(object):
    def run(self):
        print('Animal is running...')

class Dog(Animal):

    def run(self): # if we do not redefine run() in Dog class, Animal.run() will be used.
        print('Dog is running...')

    def eat(self):
        print('Eating meat...')

a = Dog()
isinstance(a, Animal) # True

'''
对于一个变量，我们只需要知道它是Animal类型，无需确切地知道它的子类型，就可以放心地调用run()方法，
而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上，由运行时该对象的确切类型决定，
这就是多态真正的威力：调用方只管调用，不管细节，而当我们新增一种Animal的子类时，只要确保run()方法编写正确，
不用管原来的代码是如何调用的。这就是著名的“开闭”原则：
    对扩展开放：允许新增Animal子类；
    对修改封闭：不需要修改依赖Animal类型的run_twice()等函数。

继承还可以一级一级地继承下来，就好比从爷爷到爸爸、再到儿子这样的关系。
而任何类，最终都可以追溯到根类object，这些继承关系看上去就像一颗倒着的树。
'''
# ====OBJECT INFO==== 
type(123) == int # True
type('string')
type(abs) # <class 'builtin_function_or_method'>
type(a) # <class '__main__.Dog'>

import types # include functions class
def fn():
    pass

type(fn)==types.FunctionType 
type(abs)==types.BuiltinFunctionType
type(lambda x: x)==types.LambdaType # anonymous func
type((x for x in range(10)))==types.GeneratorType

# to determine class of instances: 总是优先使用isinstance()判断类型，可以将指定类型及其子类“一网打尽”。
isinstance(a, Dog)==True
isinstance(a, Animal)==True
isinstance(Animal, Dog)==False
isinstance('a', str)==True

# dir() to return a list of all methods of a object
dir('ab')

len('power')=='power'.__len__ # len() actually get the __len__ of the object

class MyObject(object):

    def __init__(self):
        self.x = 9

    def power(self):
        return self.x * self.x

obj = MyObject()

# hasattr()
print('hasattr(obj, \'x\') =', hasattr(obj, 'x')) # 有属性'x'吗？
print('hasattr(obj, \'y\') =', hasattr(obj, 'y')) # 有属性'y'吗？

# setattr()
setattr(obj, 'y', 19) # 设置一个属性'y'
print('hasattr(obj, \'y\') =', hasattr(obj, 'y')) # 有属性'y'吗？

# getattr()
print('getattr(obj, \'y\') =', getattr(obj, 'y')) # 获取属性'y'
print('obj.y =', obj.y) # 获取属性'y'

print('getattr(obj, \'z\') =',getattr(obj, 'z', 404)) # 获取属性'z'，如果不存在，返回默认值404

f = getattr(obj, 'power') # 获取属性'power' method is also attributes
print(f)
print(f())

# if we can use:
sum = obj.x + obj.y
# then do not use:
sum = getattr(obj, 'x') + getattr(obj, 'y')

# ====ATRRIBUTE====
class Student3(object):
    name = 'Student'

s = Student3() # 创建实例s
print(s.name) # 打印name属性，因为实例并没有name属性，所以会继续查找class的name属性
print(Student3.name) # 打印类的name属性
s.name = 'Michael' # 给实例绑定name属性
print(s.name) # 由于实例属性优先级比类属性高，因此，它会屏蔽掉类的name属性
print(Student3.name) # 但是类属性并未消失，用Student.name仍然可以访问
del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name，由于实例的name属性没有找到，类的name属性就显示出来了

'''
从上面的例子可以看出，在编写程序的时候，千万不要对实例属性和类属性使用相同的名字，
因为相同名称的实例属性将屏蔽掉类属性，但是当你删除实例属性后，再使用相同的名称，访问到的将是类属性。
'''
class Student4(object):
    count = 0

    def __init__(self, name):
        self.name = name # instance attr (self.xx)
        Student4.count += 1 # class attr (Class_name.xx)

print(Student4.count)
bob = Student4('Bob')
print(Student4.count)
print(bob.count) # ==Student4.count
alice = Student4('Alice')
print(Student4.count)
print(bob.count) # ==Student4.count
print(alice.count) # ==Student4.count